package org.codefilarete.stalactite.engine.configurer.dslresolver;

import java.util.Collection;
import java.util.Map;
import java.util.Set;

import org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration;
import org.codefilarete.stalactite.dsl.naming.ForeignKeyNamingStrategy;
import org.codefilarete.stalactite.engine.configurer.model.AncestorJoin;
import org.codefilarete.stalactite.engine.configurer.model.DirectRelationJoin;
import org.codefilarete.stalactite.engine.configurer.model.Entity;
import org.codefilarete.stalactite.engine.configurer.model.Entity.AbstractPropertyMapping;
import org.codefilarete.stalactite.engine.configurer.model.ExtraTableJoin;
import org.codefilarete.stalactite.engine.configurer.model.Mapping;
import org.codefilarete.stalactite.engine.configurer.model.PropertyMappingHolder;
import org.codefilarete.stalactite.engine.configurer.dslresolver.InheritanceConfigurationResolver.ResolvedConfiguration;
import org.codefilarete.stalactite.engine.configurer.dslresolver.MetadataSolvingCache.EntitySource;
import org.codefilarete.stalactite.sql.ConnectionConfiguration;
import org.codefilarete.stalactite.sql.Dialect;
import org.codefilarete.stalactite.sql.ddl.structure.ForeignKey;
import org.codefilarete.stalactite.sql.ddl.structure.KeyMapping;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.tool.collection.KeepOrderMap;
import org.codefilarete.tool.collection.KeepOrderSet;
import org.codefilarete.tool.function.Hanger;

import static org.codefilarete.tool.collection.Iterables.first;

/**
 * Look up an {@link EntityMappingConfiguration} inheritance hierarchy and creates an {@link Entity} instance from it
 * with the following information set:
 * - identifier mapping
 * - property-to-column bindings
 * - joins for extra tables (secondary tables)
 * - all ancestors
 *
 * This is the first step of creating a fully configured {@link Entity} instance. The next ones are:
 * - relation
 * - polymorphism
 */
public class InheritanceMetadataResolver<C, I, T extends Table<T>> {
	
	private final Dialect dialect;
	private final ConnectionConfiguration connectionConfiguration;
	
	public InheritanceMetadataResolver(Dialect dialect, ConnectionConfiguration connectionConfiguration) {
		this.dialect = dialect;
		this.connectionConfiguration = connectionConfiguration;
	}
	
	/**
	 * Resolves the inheritance hierarchy of the given entity configuration into a ready-to-use
	 * {@link Entity} graph.
	 *
	 * @param entityConfiguration the configuration to transform into an {@link Entity} instance
	 * @return an {@link Entity} instance, filled with identifier and ancestors information
	 */
	Entity<C, I, T> resolve(EntityMappingConfiguration<C, I> entityConfiguration) {
		InheritanceConfigurationResolver<C, I> inheritanceConfigurationResolver = new InheritanceConfigurationResolver<>();
		KeepOrderSet<ResolvedConfiguration<?, I>> bottomToTopConfigurations = inheritanceConfigurationResolver.resolveConfigurations(entityConfiguration);
		return resolve(bottomToTopConfigurations).getEntity();
	}
	
	/**
	 * Resolves the inheritance hierarchy of the given entity configuration into a ready-to-use
	 * {@link Entity} graph.
	 *
	 * @param bottomToTopConfigurations the whole configuration hierarchy, ordered from bottom to top, the first one will be transformed into an {@link Entity} instance 
	 * @return an {@link Entity} instance, filled with identifier and ancestors information
	 */
	EntitySource<C, I> resolve(KeepOrderSet<ResolvedConfiguration<?, I>> bottomToTopConfigurations) {
		KeyMappingApplier<C, I> keyMappingApplier = new KeyMappingApplier<>(dialect, connectionConfiguration);
		keyMappingApplier.resolve(bottomToTopConfigurations);
		
		return resolveHierarchy(bottomToTopConfigurations);
	}
	
	<X, TT extends Table<TT>> EntitySource<C, I> resolveHierarchy(KeepOrderSet<ResolvedConfiguration<?, I>> bottomToTopConfigurations) {
		// Handling very first entity as a seed for eventual next iterations on the hierarchy
		ResolvedConfiguration<C, I> bottomestConfiguration = (ResolvedConfiguration<C, I>) first(bottomToTopConfigurations);
		T bottomTable = (T) bottomestConfiguration.getTable();
		Entity<C, I, T> bottomestEntity = this.buildEntity(bottomestConfiguration, bottomTable);
		EntitySource<C, I> bottomestEntitySource = new EntitySource<>(bottomestEntity, bottomestConfiguration);
		
		final Hanger.Holder<TT> previousTable = new Hanger.Holder<>((TT) bottomTable);
		final Hanger.Holder<Entity<X, I, TT>> previousEntity = new Hanger.Holder<>((Entity<X, I, TT>) bottomestEntity);
		bottomToTopConfigurations.stream().skip(1)
				.map(((Class<ResolvedConfiguration<X, I>>) (Class) ResolvedConfiguration.class)::cast)
				.forEach(resolvedConfigurationPawn -> {
					TT resolvedTable = (TT) resolvedConfigurationPawn.getTable();
					if (previousTable.get() != resolvedTable) {
						Entity<X, I, TT> ancestor = buildEntity(resolvedConfigurationPawn, resolvedTable);
						bottomestEntitySource.addSource(ancestor, resolvedConfigurationPawn);
						// We could use the foreign key between previousTable and resolvedTable that has been created by the KeyMappingApplier instance
						// used during buildEntity(..) logic, but the foreign key is not exposed, thus we mimic it through the primary keys (should be enhanced)
						previousEntity.get().setParent(new AncestorJoin<>(ancestor, new DirectRelationJoin<>(previousTable.get().getPrimaryKey(), resolvedTable.getPrimaryKey())));
						// preparing next iteration
						previousTable.set(resolvedTable);
						previousEntity.set(ancestor);
					} else {
						addMapping(previousEntity.get(), resolvedConfigurationPawn, previousTable.get());
						bottomestEntitySource.addSource(previousEntity.get(), resolvedConfigurationPawn);
					}
				});
		return bottomestEntitySource;
	}
	
	private <X, TT extends Table<TT>> Entity<X, I, TT> buildEntity(ResolvedConfiguration<X, I> configuration, TT table) {
		Entity<X, I, TT> result = new Entity<>(configuration.getIdentifierMapping(), new Mapping<>(configuration.getMappingConfiguration().getEntityType(), table));
		addMapping(result, configuration, table);
		return result;
	}
	
	private <X, TT extends Table<TT>, EXTRATABLE extends Table<EXTRATABLE>> void addMapping(Entity<X, I, TT> entity,
	                                                                                         ResolvedConfiguration<X, I> configuration,
	                                                                                         TT table) {
		PropertyMappingResolver<X, TT> propertyMappingResolver = new PropertyMappingResolver<>(dialect.getColumnBinderRegistry());
		Set<AbstractPropertyMapping<X, ?, TT>> mapping = propertyMappingResolver.resolve(
				configuration.getMappingConfiguration().getPropertiesMapping(),
				table,
				configuration.getNamingConfiguration().getColumnNamingStrategy());
		
		Set<AbstractPropertyMapping<X, ?, EXTRATABLE>> extraTableMappings = new KeepOrderSet<>();
		mapping.forEach(mappingPawn -> {
			if (mappingPawn.getColumn().getTable() == table) {
				entity.getPropertyMappingHolder().addMapping(mappingPawn);
			} else {
				extraTableMappings.add((AbstractPropertyMapping<X, ?, EXTRATABLE>) mappingPawn);
			}
		});
		addExtraTableProperties(entity, extraTableMappings, configuration.getNamingConfiguration().getForeignKeyNamingStrategy());
		
	}
	
	private <X, TT extends Table<TT>, EXTRATABLE extends Table<EXTRATABLE>> void addExtraTableProperties(Entity<X, I, TT> result,
	                                                                                                     Collection<AbstractPropertyMapping<X, ?, EXTRATABLE>> extraTableProperties,
	                                                                                                     ForeignKeyNamingStrategy foreignKeyNamingStrategy) {
		Map<EXTRATABLE, KeepOrderSet<AbstractPropertyMapping<X, ?, EXTRATABLE>>> propertiesPerTable = new KeepOrderMap<>();
		extraTableProperties.forEach(mapping -> {
			KeepOrderSet<AbstractPropertyMapping<X, ?, EXTRATABLE>> mappings = propertiesPerTable.computeIfAbsent(mapping.getColumn().getTable(), k -> new KeepOrderSet<>());
			mappings.add(mapping);
		});
		propertiesPerTable.forEach((extraTable, mapping) -> {
			PrimaryKeyPropagator<TT, EXTRATABLE, I> primaryKeyPropagator = new PrimaryKeyPropagator<>();
			ForeignKey<EXTRATABLE, TT, I> foreignKey = primaryKeyPropagator.propagate(result.getTable().getPrimaryKey(), extraTable, foreignKeyNamingStrategy);
			PropertyMappingHolder<X, EXTRATABLE> mappingHolder = new PropertyMappingHolder<>();
			mappingHolder.addMapping(mapping);
			KeyMapping<TT, EXTRATABLE, I> reference = foreignKey.getReferencedKey().reference(foreignKey);
			ExtraTableJoin<X, TT, EXTRATABLE, I> relation = new ExtraTableJoin<>(mappingHolder, reference);
			result.addRelation(relation);
		});
	}
}
